#!/bin/sh -efu
#
# Copyright (C) 2006-2021  Dmitry V. Levin <ldv@altlinux.org>
# Copyright (C) 2006-2008  Alexey I. Froloff <raorn@altlinux.org>
# Copyright (C) 2006-2021  Alexey Gladkov <legion@altlinux.org>
# Copyright (C) 2006-2007  Sergey Vlasov <vsu@altlinux.org>
# Copyright (C) 2006  Fr. Br. George <george@altlinux.org>
#
# Get Every Archive from git package Repository.
#
# This file is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
#

# Note about rules:
#  rules is plain text file, where each meaningful line has following format:
#  directive: args
#
#  Each directive has own args syntax:
#  spec: path_to_file
#  specsubst: variables..
#  tags: path_to_directory
#  (copy|gzip|bzip2|lzma|xz|zstd): glob_pattern..
#  (copy|gzip|bzip2|lzma|xz|zstd)?: glob_pattern..
#  exclude: glob_pattern..
#  tar(|.gz|.bz2|.lzma|.xz|.zst): tree_path [options]
#  tar(|.gz|.bz2|.lzma|.xz|.zst)?: tree_path [options]
#  zip: tree_path [options]
#  zip?: tree_path [options]
#  diff(|.gz|.bz2|.lzma|.xz|.zst): old_tree_path new_tree_path options
#  diff(|.gz|.bz2|.lzma|.xz|.zst)?: old_tree_path new_tree_path options
#
#  tree_path may be a plain path or commit:path, where commit is specified
#  by tag name (keywords may be used) or full SHA-1.
#
#  Valid tar and zip options are:
#    name=archive_name  - archive name, may reference to keywords;
#    base=base_name     - when specified it is added as a leading path
#                         to the files in the generated tar archive;
#    spec=path_to_file  - path to specfile which defines keywords;
#    suffix=suffix      - override default output file name suffix.
#  Valid tar keywords are:
#    @dir@ - basename(path(tree_path));
#    @name@, @version@, @release@.
#  Default tar archive name is @dir@-@version@.
#
#  Valid diff options are:
#    name=diff_name     - diff name, may reference to keywords;
#    spec=path_to_file  - path to specfile which defines keywords.
#  Valid diff keywords are:
#    @old_dir@ - basename(path(old_tree_path));
#    @new_dir@ - basename(path(new_tree_path));
#    @name@, @version@, @release@.
#  Default diff name is @new_dir@-@version@-@release@.patch.

. gear-utils-sh-functions

print_version()
{
	cat <<EOF
$PROG version $PROG_VERSION
Written by Dmitry V. Levin <ldv@altlinux.org>

Copyright (C) 2006-2021  Dmitry V. Levin <ldv@altlinux.org>
Copyright (C) 2006-2008  Alexey I. Froloff <raorn@altlinux.org>
Copyright (C) 2006-2021  Alexey Gladkov <legion@altlinux.org>
Copyright (C) 2006-2007  Sergey Vlasov <vsu@altlinux.org>
Copyright (C) 2006  Fr. Br. George <george@altlinux.org>
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
EOF
	exit
}

show_help()
{
	cat <<EOF
$PROG - extract archive from git package repository.

Usage: $PROG [options] <output-tarball-name>
or:    $PROG [options] --export-dir=<dirname>
or:    $PROG [options] --command -- <command>...
or:    $PROG [options] --hasher -- <hsh-command>...
or:    $PROG [options] --rpmbuild -- <rpmbuld-command>...

Options:
  --no-compress             do not compress output archive;
  --bzip2                   compress output archive using bzip2;
  --gzip                    compress output archive using gzip;
  --lz4                     compress output archive using lz4;
  --lzop                    compress output archive using lzop;
  --xz                      compress output archive using xz;
  --zstd                    compress output archive using zstd;
  --disable-specsubst       disable specsubst directive support;
  --commit                  make temporary commit prior to extract;
  --command                 execute arbitrary command afterwards;
  --hasher                  execute hsh-like command afterwards;
  --rpmbuild                execute rpmbuild-like command afterwards;
  --update-spec             if specfile was changed after executed rpmbuild
                            command, then update it in the git repository;
  --export-dir=DIRNAME      write source files to specified directory;
  --describe                describe package as "%{NAME} %{VERSION} %{RELEASE}";
  -r, --rules=FILENAME      override rules file name;
  -t, --tree-ish=ID         COMMIT[:PATH] specifying the tree to process;
  -q, --quiet               try to be more quiet;
  -v, --verbose             print a message for each action;
  -V, --version             print program version and exit;
  -h, --help                show this text and exit.

Report bugs to http://bugzilla.altlinux.org/

EOF
	exit
}

tmpdir=
cleanup_handler()
{
	[ -z "$tmpdir" ] ||
		rm -rf -- "$tmpdir"
}

write_compressed_file()
{
	local file="$outdir/${1##*/}"; shift
	local name="${1##*/}"; shift
	local cmd="$1"; shift

	[ -z "$name" ] ||
		name="$outdir/$name"

	case "$cmd" in
		gzip|bzip2|lzma|xz|zstd)
			type "$cmd" >/dev/null 2>&1 ||
				rules_error "Compress command not found: $cmd"
			if [ $# -eq 0 ]; then
				case "$cmd" in
					gzip) set -- -9n ;;
					zstd) set -- -11 -T0 ;;
					xz) set -- -9 --memlimit-compress=100% -T0 ;;
					*) set -- -9 ;;
				esac
			fi
			if [ -n "$name" ]; then
				local tmp_name
				tmp_name="$(mktemp -- "$name".XXXXXX)"
				$cmd "$@" <"$file" >"$tmp_name"
				rm -- "$file"
				mv -f -- "$tmp_name" "$name"
				verbose "Compressed file \`${file##*/}' into \`${name##*/}' using \`$cmd $*'"
			else
				case "$cmd" in
					zstd) cmd="$cmd -q --rm" ;;
				esac
				$cmd "$@" "$file" </dev/null >/dev/null
				verbose "Compressed file \`${file##*/}' using \`$cmd $*'"
			fi
			;;
		copy)
			[ $# -eq 0 ] ||
				rules_error "Unsupported options for copy method specified: $*"
			if [ -n "$name" ] && [ "$file" != "$name" ]; then
				mv -f -- "$file" "$name"
				verbose "Wrote file \`${file##*/}' as \`${name##*/}'"
			fi
			;;
		*)
			rules_error "Unrecognized compress type specified: $cmd"
			;;
	esac
}

compress_file()
{
	local cmd="$1" && shift
	local file="$1" && shift
	local dest="$1" && shift

	case "$cmd" in
		tar|zip)
			write_compressed_file "$file" "$dest" copy
			;;
		gzip|*.gz)
			write_compressed_file "$file" "$dest" gzip
			;;
		bzip2|*.bz2)
			write_compressed_file "$file" "$dest" bzip2
			;;
		lzma|*.lzma)
			write_compressed_file "$file" "$dest" lzma
			;;
		xz|*.xz)
			write_compressed_file "$file" "$dest" xz
			;;
		zstd|*.zst)
			write_compressed_file "$file" "$dest" zstd
			;;
	esac
}

subst_NVR_from_spec()
{
	local spec="$1" && shift

	if [ -n "$spec" ]; then
		cat_blob "$main_tree_id" "$spec" >"$workdir/spec"
		spec="$workdir/spec"
	fi
	subst_NVR_from_spec_file "$spec" "$@"
}

parse_exclude_option()
{
	local name="$1" && shift
	local ret="$1" && shift
	local opt="$1" && shift

	opt="${opt#exclude=}"

	[ -n "$opt" ] ||
		rules_error "Invalid $name exclude pattern specified: empty pattern"

	valid_string "$opt" ||
		rules_error "Invalid $name exclude pattern specified: $opt"

	eval "$ret=\"\$$ret :(exclude,glob)\$opt\""
}

get_archive_opts()
{
	local format="$1" && shift

	local dir_name_prefix="${dir_name%/*}"
	local dir_name_base="${dir_name##*/}"

	if [ -z "$dir_name_prefix" ] || [ "$dir_name" = "$dir_name_prefix" ]; then
		dir_name_prefix=
	else
		dir_name_prefix="$dir_name_prefix/"
	fi
	# "$dir_name" == "$dir_name_prefix$dir_name_base"

	local dir="$dir_name_base"
	[ "$dir" != . ] || dir=
	if [ -z "$dir" ]; then
		tar_name='@name@-@version@'
	else
		tar_name='@dir@-@version@'
	fi

	local opt spec='' tar_base_set='' tar_suffix_set=''

	for opt; do
		case "$opt" in
			spec=*) spec="${opt#spec=}"
				check_pathname specfile "$spec"
				;;
			name=*) tar_name="${opt#name=}"
				;;
			base=*) tar_base="${opt#base=}"
				tar_base_set=1
				;;
			suffix=*) tar_suffix="${opt#suffix=}"
				tar_suffix_set=1
				;;
			exclude=*)
				parse_exclude_option "$format" archive_exclude "$opt"
				;;
			*) rules_error "Unrecognized option: $opt"
				;;
		esac
	done

	local subst_vars='tar_name tar_tree'
	[ -z "$tar_base" ] || [ -z "$tar_base_set" ] ||
		subst_vars="$subst_vars tar_base"
	[ -z "$tar_suffix" ] || [ -z "$tar_suffix_set" ] ||
		subst_vars="$subst_vars tar_suffix"

	subst_NVR_from_spec "$spec" dir $subst_vars
	subst_key_in_vars '@dir@' "$dir" $subst_vars

	if [ -n "$dir" ] && [ "$dir" != "$dir_name_base" ]; then
		check_filename 'dir name' "$dir"
		dir_name="$dir_name_prefix$dir"
		check_pathname 'dir path' "$dir_name"
	fi
	check_filename "$format name" "$tar_name"
	[ -n "${subst_vars##*tar_base*}" ] ||
		check_pathname "$format base" "$tar_base"
	[ -n "${subst_vars##*tar_suffix*}" ] ||
		check_filename "$format suffix" "$tar_suffix"
}

write_git_archive()
{
	local format="$1" && shift
	local optional="$1" && shift
	local cmd="$1" && shift
	local exclude="$1" && shift
	local tree="$1" && shift
	local dir="$1" && shift
	local name="${1##*/}" && shift
	local tar_base="$1" && shift
	local suffix="$1" && shift

	if [ "$tar_base" = '/' ]; then
		tar_base="$name"
	fi

	local id
	id="$(traverse_tree "$tree" "$dir" "$optional")" ||
		{
			[ $? -gt 1 ] && return 0 ||
				exit 1
		}
		rc=$?

	git archive --format="$format" ${tar_base:+--prefix="$tar_base"/} \
		"$id" -- $exclude >"$outdir/$name.$format"
	verbose "Extracted archive: $name.$format"

	local output_name=
	if [ "$suffix" != '/' ]; then
		output_name="$name$suffix"
	fi
	compress_file "$cmd" "$name.$format" "$output_name"
}

make_archive()
{
	# format: "format $optional cmd dirname options.."
	[ $# -ge 4 ] ||
		rules_error 'No dirname specified'
	local format="$1" && shift
	local optional="$1" && shift
	local cmd="$1" && shift
	local tree_path="$1" && shift

	tree_path="$(parse_tree_path dirname "$tree_path")" || return 1
	local dir_name="${tree_path#*:}"
	tar_tree="${tree_path%%:*}"

	local tar_base tar_suffix archive_exclude
	# Special non-empty marker.
	tar_base='/'
	tar_suffix='/'
	archive_exclude=''
	get_archive_opts "$format" "$@"

	tar_tree="$(resolve_tree_base_name "$tar_tree")" || return 1
	write_git_archive "$format" "$optional" "$cmd" "$archive_exclude" \
		"$tar_tree" "$dir_name" "$tar_name" "$tar_base" "$tar_suffix"
}

get_diff_opts()
{
	local old_d="$1" && shift
	local new_d="$1" && shift

	local old_d_prefix="${old_d%/*}"
	if [ -z "$old_d_prefix" ] || [ "$old_d" = "$old_d_prefix" ]; then
		old_d_prefix=
	else
		old_d_prefix="$old_d_prefix/"
	fi
	old_d="${old_d##*/}"

	local new_d_prefix="${new_d%/*}"
	if [ -z "$new_d_prefix" ] || [ "$new_d" = "$new_d_prefix" ]; then
		new_d_prefix=
	else
		new_d_prefix="$new_d_prefix/"
	fi
	new_d="${new_d##*/}"

	[ "$old_d" != '.' ] || old_d=
	[ "$new_d" != '.' ] || new_d=

	if [ -z "$new_d" ]; then
		diff_name='@name@-@version@-@release@.patch'
	else
		diff_name='@new_dir@-@version@-@release@.patch'
	fi

	local opt spec=

	for opt; do
		case "$opt" in
			spec=*) spec="${opt#spec=}"
				check_pathname specfile "$spec"
				;;
			name=*) diff_name="${opt#name=}"
				;;
			exclude=*)
				parse_exclude_option diff diff_exclude "$opt"
				;;
			*) rules_error "Unrecognized option: $opt"
				;;
		esac
	done

	local subst_vars='diff_name diff_old_tree diff_new_tree'
	subst_key_in_vars '@old_dir@' "$old_d" $subst_vars
	subst_key_in_vars '@new_dir@' "$new_d" $subst_vars
	subst_NVR_from_spec "$spec" old_d new_d $subst_vars
	check_filename 'diff name' "$diff_name"
	if [ -n "$old_d" ] && [ "$old_d" != "$old_dir" ]; then
		check_filename 'old dir name' "$old_d"
		old_dir="$old_d_prefix$old_d"
		check_pathname 'old dir path' "$old_dir"
	fi
	if [ -n "$new_d" ] && [ "$new_d" != "$new_dir" ]; then
		check_filename 'new dir name' "$new_d"
		new_dir="$new_d_prefix$new_d"
		check_pathname 'new dir path' "$new_dir"
	fi
}

write_git_diff()
{
	local optional="$1" && shift
	local cmd="$1" && shift
	local diff_exclude="$1" && shift
	local old_tree="$1" && shift
	local old_dir="$1" && shift
	local new_tree="$1" && shift
	local new_dir="$1" && shift
	local name="${1##*/}" && shift

	local old_id new_id

	old_id="$(traverse_tree "$old_tree" "$old_dir" "$optional")" ||
		{
			[ $? -gt 1 ] && return 0 ||
				exit 1
		}

	new_id="$(traverse_tree "$new_tree" "$new_dir" "$optional")" ||
		{
			[ $? -gt 1 ] && return 0 ||
				exit 1
		}

	git diff-tree --patch-with-stat --text --no-renames --no-ext-diff \
		"$old_id" "$new_id" -- $diff_exclude >"$outdir/$name"
	verbose "Extracted diff: $name"

	compress_file "$cmd" "$name" ''
}

make_difference()
{
	# format: "$optional cmd old_dir new_dir options.."
	[ $# -ge 4 ] ||
		rules_error 'No old_dir or new_dir specified'
	local optional="$1" && shift
	local cmd="$1" && shift
	local old_dir="$1" && shift
	local new_dir="$1" && shift

	old_dir="$(parse_tree_path dirname "$old_dir")" ||
		rules_error "Invalid old_dir specified: $old_dir"
	diff_old_tree="${old_dir%%:*}"
	old_dir="${old_dir#*:}"

	new_dir="$(parse_tree_path dirname "$new_dir")" ||
		rules_error "Invalid new_dir specified: $new_dir"
	diff_new_tree="${new_dir%%:*}"
	new_dir="${new_dir#*:}"

	local diff_exclude=
	get_diff_opts "$old_dir" "$new_dir" "$@"

	diff_old_tree="$(resolve_tree_base_name "$diff_old_tree")" ||
		rules_error "Invalid old tree: $diff_old_tree"
	diff_new_tree="$(resolve_tree_base_name "$diff_new_tree")" ||
		rules_error "Invalid new tree: $diff_new_tree"

	write_git_diff \
		"$optional" "$cmd" "$diff_exclude" \
		"$diff_old_tree" "$old_dir" \
		"$diff_new_tree" "$new_dir" "$diff_name"
}

extract_pattern()
{
	# format: "$optional pattern target_name cmd [args]"
	local optional="$1"; shift
	local pattern="$1"; shift
	local target_name="$1"; shift
	local cmd="$1"; shift

	valid_string "$pattern" ||
		rules_error "Invalid extract pattern specified: $pattern"

	local dir_name base_name tree

	dir_name="$(dirname "$pattern")"
	base_name="$(basename "$pattern")"
	tree="$(traverse_tree "$main_tree_id" "$dir_name" "$optional")" ||
		{
			[ $? -gt 1 ] && return 0 ||
				exit 1
		}
	git ls-tree "$tree" >"$workdir/blobs"
	echo >>"$workdir/blobs"

	local mode otype id name found=
	while read -r mode otype id name; do
		# ignore non-blobs.
		[ "$otype" = blob ] ||
			continue
		# ignore invalid filenames.
		[ "$name" = "${name#*/}" ] &&
		    [ "$name" = "${name%/*}" ] ||
			continue
		# ignore unmatched.
		[ -z "${name##$base_name}" ] ||
		    [ -z "${name%%$base_name}" ] ||
			continue

		local ex_pattern sample
		sample="$dir_name/$name"
		for ex_pattern in $exclude_pattern_list; do
			# ignore excluded.
			[ -n "${sample##$ex_pattern}" ] &&
			    [ -n "${sample%%$ex_pattern}" ] ||
				continue 2
		done

		git cat-file blob "$id" >"$outdir/$name"
		verbose "Extracted file by pattern \"$pattern\": $name"
		[ -n "${mode%%*7??}" ] ||
			chmod a+x "$outdir/$name"
		write_compressed_file "$name" "$target_name" "$cmd" "$@"
		found=1
	done <"$workdir/blobs"
	[ -n "$found" ] || [ "$optional" = 1 ] ||
		rules_error "Unmatched pattern specified: $pattern"
}

make_compress()
{
	# format: "$optional options -- patterns..."
	local optional="$1"; shift
	local target_name='' cmd='' args=''

	while [ $# -gt 0 ]; do
		case "$1" in
			name=*)
				target_name="${1#name=}"
				subst_NVR_from_spec_file '' target_name
				check_filename 'target name' "$target_name"
				;;
			type=*)
				cmd="${1#type=}"
				;;
			--)
				shift; break
				;;
			*)
				[ -n "$1" ] && [ -z "${1##-*}" ] ||
					rules_error "Invalid option specified: $1"
				check_filename 'command args' "$1"
				args="$args \"$(quote_shell "$1")\""
				;;
		esac
		shift
	done
	[ $# -gt 0 ] ||
		rules_error 'No pattern specified'
	[ -n "$cmd" ] ||
		rules_error 'No extract type specified'

	local pattern
	for pattern; do
		eval extract_pattern "\"$optional\"" \
			"\"$(quote_shell "$pattern")\"" \
			"\"$(quote_shell "$target_name")\"" \
			"\"$(quote_shell "$cmd")\"" $args
	done
}

parse_rules()
{
	[ -s "$workdir/rules" ] || return 0

	exclude_pattern_list=
	lineno=0

	local cmd options
	while read -r cmd options; do
		lineno="$((lineno+1))"
		if [ "$cmd" = 'exclude:' ]; then
			valid_string "$options" ||
				rules_error "Invalid exclude pattern specified: $options"
			exclude_pattern_list="$exclude_pattern_list $options"
		fi
	done <"$workdir/rules"

	lineno=0
	while read -r cmd options; do
		lineno="$((lineno+1))"
		local optional=0
		case "$cmd" in
			""|\#*)
				continue
				;;
			*\?:)
				cmd="${cmd%\?:}"
				optional=1
				;;
			*:)
				cmd="${cmd%:}"
				optional=0
				;;
			*)
				rules_info "Unrecognized rule ignored: $cmd"
				continue
				;;
		esac

		local opts
		quote_shell_args opts "$options"
		eval "set -- $opts"

		case "$cmd" in
			spec|specsubst|exclude|tags)
				continue
				;;
			tar|tar.gz|tar.bz2|tar.lzma|tar.xz|tar.zst)
				make_archive 'tar' "$optional" "$cmd" "$@" ||
					rules_error 'Failed to make archive'
				;;
			zip)
				make_archive 'zip' "$optional" "$cmd" "$@" ||
					rules_error 'Failed to make archive'
				;;
			diff|diff.gz|diff.bz2|diff.lzma|diff.xz|diff.zst)
				make_difference "$optional" "$cmd" "$@" ||
					rules_error 'Failed to make diff'
				;;
			copy|gzip|bzip2|lzma|xz|zstd)
				make_compress "$optional" "type=$cmd" -- "$@" ||
					rules_error 'Failed to extract by pattern'
				;;
			compress)
				make_compress "$optional" "$@" ||
					rules_error 'Failed to extract by pattern'
				;;
			*)
				rules_info "Unrecognized directive ignored: $cmd"
				continue
				;;
		esac
	done <"$workdir/rules"
	lineno=
}

TEMP=`getopt -n $PROG -o "$gear_short_options" -l "$gear_long_options" -- "$@"` ||
	show_usage
eval set -- "$TEMP"

hasher=
update_spec=
rpmbuild=
command=
command_helper='gear-command-tar'
outdir=
describe=
do_commit=
main_tree_id=HEAD
gear_config_option tar_compress compress --gzip
gear_config_option rules rules "$rules"
while :; do
	case "$1" in
		--) shift; break
			;;
		--no-compress) tar_compress=
			;;
		--commit) do_commit=1
			;;
		--bzip2) tar_compress=--bzip2
			;;
		--gzip) tar_compress=--gzip
			;;
		--lz4) tar_compress=--lz4
			;;
		--lzop) tar_compress=--lzop
			;;
		--xz) tar_compress=--xz
			;;
		--zstd) tar_compress=--zstd
			;;
		--disable-specsubst) disable_specsubst=1
			;;
		--command) command=1; command_helper=
			;;
		--hasher) hasher=1
			command_helper='gear-command-hasher'
			;;
		--rpmbuild) rpmbuild=1
			command_helper='gear-command-rpmbuild'
			;;
		--update-spec) update_spec=1
			;;
		--export-dir)
			outdir="$(opt_check_dir "$1" "$2")"
			shift
			command_helper=
			;;
		--describe) describe=1
			;;
		-r|--rules) shift; rules="$1"
			;;
		-t|--tree-ish) shift; main_tree_id="$1"
			;;
		-h|--help) show_help
			;;
		-q|--quiet) quiet=-q; verbose=
			;;
		-v|--verbose) verbose=-v; quiet=
			;;
		-V|--version) print_version
			;;
		*) fatal "Unrecognized option: $1"
			;;
	esac
	shift
done

[ -z "$do_commit" ] || [ "${main_tree_id%%:*}" = HEAD ] ||
	show_usage 'Option --commit is allowed for HEAD commit only.'

case "$command$describe$hasher${outdir:+1}$rpmbuild" in
	''|1) ;;
	*) show_usage 'Options --hasher, --rpmbuild, --export-dir, --command and --describe are mutually exclusive.' ;;
esac

out_file=
if [ -n "$hasher$rpmbuild$command" ]; then
	# At least one argument, please.
	[ $# -ge 1 ] ||
		show_usage 'Not enough arguments.'
elif [ -n "$outdir$describe" ]; then
	# No arguments, please.
	[ $# -eq 0 ] ||
		show_usage 'Too many arguments.'
else
	# Exactly one argument, please.
	[ $# -ge 1 ] ||
		show_usage 'Not enough arguments.'
	[ $# -eq 1 ] ||
		show_usage 'Too many arguments.'
	out_file="$1"
	shift
fi

# Save git directory for future use.
git_dir="$(git rev-parse --git-dir)"
git_dir="$(readlink -ev -- "$git_dir")"

# Change to toplevel directory.
chdir_to_toplevel

# Check given tree-ish.
main_commit_id="${main_tree_id%%:*}"
main_commit_sha1="$(get_commit_sha1 "$main_commit_id")" ||
	fatal "Invalid commit \"$main_commit_id\""
main_tag_sha1="$(get_tag_sha1 "$main_commit_id" ||:)"
main_tag_id="$main_commit_id"

if [ -z "$do_commit" ]; then
	get_tree_sha1 "$main_tree_id" > /dev/null ||
		fatal "Invalid tree \"$main_tree_id\" specified"
fi

if [ -n "$update_spec" ]; then
	[ -n "$rpmbuild$command" ] ||
		fatal 'Unable to update specfile without appropriate command.'
fi

install_cleanup_handler cleanup_handler
tmpdir="$(mktemp -dt "$PROG.XXXXXXXX")"
workdir="$tmpdir/work"
mkdir "$workdir"
if [ -z "$outdir" ]; then
	outdir="$tmpdir/out"
	mkdir $verbose "$outdir"
fi

create_temporary_git_object_directory()
{
	local orig_object_dir
	orig_object_dir="${GIT_OBJECT_DIRECTORY-"$(git rev-parse --git-path objects)"}"
	orig_object_dir="$(cd "$orig_object_dir" && pwd)"
	mkdir -p "$tmpdir/objects/info"
	printf %s\\n "$orig_object_dir" >"$tmpdir/objects/info/alternates"
	GIT_OBJECT_DIRECTORY="$tmpdir/objects"
	gear_export_vars GIT_OBJECT_DIRECTORY
}

make_temporary_tag()
{
	get_specsubst_from_rules
	[ -n "$specsubst" ] || return 0

	cat > "$tmpdir/mktag" <<EOF
object $main_commit_sha1
type commit
tag gear--commit
tagger $(git cat-file commit $main_commit_sha1 |sed -r '/^committer +/!d;s///;q')

Temporary tag by $PROG.

EOF
	local opts var value
	quote_shell_args opts "$specsubst"
	eval "set -- $opts"

	for var; do
		opts="gear.specsubst.$var"
		value="$(git config --get "$opts")" ||
			fatal "failed to prepare temporary tag: git config option \"$opts\" not found"
		printf 'X-gear-specsubst: %s=%s\n' "$var" "$value"
	done >> "$tmpdir/mktag"

	main_tag_sha1="$(git mktag < "$tmpdir/mktag")"
	main_tag_id="$main_tag_sha1"
	verbose 'Temporarily tagged local changes.'
}

if [ -n "$do_commit" ]; then
	create_temporary_git_object_directory

	cp "${GIT_INDEX_FILE-"$git_dir/index"}" "$tmpdir/GIT_INDEX_FILE"
	GIT_INDEX_FILE="$tmpdir/GIT_INDEX_FILE"
	gear_export_vars GIT_INDEX_FILE

	git diff-files --name-only -z |
		git update-index --remove -z --stdin
	git update-index -q --refresh

	tmp_tree="$(git write-tree)"
	if [ "$tmp_tree" = "$(git rev-parse "$main_commit_sha1^{tree}" 2>/dev/null)" ]; then
		get_tree_sha1 "$main_tree_id" > /dev/null ||
			fatal "Invalid tree \"$main_tree_id\" specified"
		verbose 'Nothing to commit'
	else
		tmp_parents="-p $main_commit_sha1"
		if [ -f "$git_dir/MERGE_HEAD" ]; then
			tmp_parents="$tmp_parents $(sed 's/^/-p /' "$git_dir/MERGE_HEAD")"
		fi
		main_commit_id="$(echo "Temporary commit by $PROG." |
			git commit-tree "$tmp_tree" $tmp_parents)"
		main_commit_sha1="$main_commit_id"
		if [ -z "${main_tree_id##*:*}" ]; then
			tmp_main_tree_id="$main_commit_id:${main_tree_id#*:}"
		else
			tmp_main_tree_id="$main_commit_id"
		fi
		get_tree_sha1 "$tmp_main_tree_id" > /dev/null ||
			fatal "Invalid tree \"$main_tree_id\" specified"
		main_tree_id="$tmp_main_tree_id"
		verbose 'Temporarily committed local changes.'
	fi

	make_temporary_tag
fi

find_specfile

if [ -n "$describe" ]; then
	printf '%s %s %s\n' "$pkg_name" "$pkg_version" "$pkg_release"
	exit 0
fi

if [ -z "$do_commit" ]; then
	create_temporary_git_object_directory
fi

find_tags_in_tree
parse_rules

install -pm644 "$workdir/specfile" "$outdir/${specfile##*/}"
verbose "Extracted specfile: ${specfile##*/}"

if [ -n "$update_spec" ]; then
	[ -f "$specfile" ] ||
		fatal "Unable to update specfile: file $specfile not available."
	cmp -s "$specfile" "$workdir/specfile" ||
		fatal "Unable to update specfile: file $specfile differs from original specfile."
fi

[ -n "$command_helper$*" ] ||
	exit 0

SOURCE_DATE_EPOCH=
if [ -n "${main_tag_sha1-}" ]; then
	SOURCE_DATE_EPOCH="$(git cat-file tag "$main_tag_sha1" |
		sed -r '/^tagger[[:space:]]+/!d;s///;q' |
		sed -rn 's/^[^>]+>[[:space:]]+([0-9]+).*/\1/p')"
	date -u -d "@$SOURCE_DATE_EPOCH" > /dev/null ||
		fatal "Failed to infer a tagger timestamp from tag \"$main_tag_id\"."
elif [ -n "${main_commit_sha1-}" ]; then
	SOURCE_DATE_EPOCH="$(git cat-file commit "$main_commit_sha1" |
		sed -r '/^committer[[:space:]]+/!d;s///;q' |
		sed -rn 's/^[^>]+>[[:space:]]+([0-9]+).*/\1/p')"
	date -u -d "@$SOURCE_DATE_EPOCH" > /dev/null ||
		fatal "Failed to infer a committer timestamp from commit \"$main_commit_id\"."
fi
[ -n "$SOURCE_DATE_EPOCH" ] &&
	export SOURCE_DATE_EPOCH ||
	unset SOURCE_DATE_EPOCH

gear_commit_id="$(get_object_sha1 "$main_commit_id")" ||
	fatal 'Failed to parse commit.'
export gear_commit_id

gear_pkg_name="$pkg_name"
gear_pkg_version="$pkg_version"
gear_pkg_release="$pkg_release"
export gear_pkg_name gear_pkg_version gear_pkg_release

gear_outdir="$outdir"
gear_outfile="${out_file:-$workdir/pkg.tar}"
gear_specfile="$specfile"
gear_tmpdir="$tmpdir"
gear_workdir="$workdir"
export gear_outdir gear_outfile gear_specfile gear_tmpdir gear_workdir

gear_rules="$rules"
gear_tar_compress="$tar_compress"
gear_update_spec="$update_spec"
gear_quiet="$quiet"
gear_verbose="$verbose"
export gear_rules gear_tar_compress gear_update_spec gear_quiet gear_verbose

# In gear --update-spec mode the path to specfile has to be absolute.
if [ -n "$update_spec" ] && [ -n "${gear_specfile##/*}" ]; then
	gear_specfile="$(readlink -fv -- "$gear_specfile")"
fi

# Change the cwd back if it was changed earlier by chdir_to_toplevel.
chdir_back_from_toplevel

run_command $command_helper "$@"
